﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.IO
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;

    /// <summary>Transparently encodes or decodes data to/from quoted-printable.</summary>
    /// <remarks>
    /// <para>When data is written to this stream then it is first encoded and the encoded form is then written to the
    /// underlying stream that was passed to the constructor. A call to any of the Read methods of this stream takes
    /// data read from the underlying stream. The data is then decoded and the decoded form is then returned. For both
    /// cases a separate buffer of the size specified at construction is used, so calls to the Read/Write methods of
    /// this class do not directly translate to calls to the equivalent methods of the underlying stream.</para>
    /// <para>This class aims to be a fully conformant implementation of the rules (1)-(5) documented in
    /// <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a> under section 6.7., including the stripping of any
    /// white space that appears at the end of the read lines. Moreover, this class also implements the robustness
    /// suggestions (1), (2), (3) and (5) documented on page 22 of the same document. Instead of suggestion (4), this
    /// class simply passes through unaltered invalid octets read from the underlying stream.</para>
    /// </remarks>
    /// <threadsafety static="true" instance="false"/>
    public sealed class QuotedPrintableStream : BufferStream
    {
        private const int MaxEncodedLineLength = 78; // Includes CRLF

        private static readonly byte[] DecodeTable = new byte[]
        {
         // 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x00
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x10
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x20
            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x30
            0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x40
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x50
            0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x60
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x70
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x80
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x90
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xa0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xb0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xc0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xd0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xe0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff  // 0xf0
        };

        private static readonly byte[] EncodeTable = new byte[]
        {
         // 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
            0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46
        };

        // See http://www.ietf.org/rfc/rfc2045.txt, 6.7.
        private static readonly bool[] Rfc2045QuoteTable = new bool[]
        {
         // 0x00,  0x01,  0x02,  0x03,  0x04,  0x05,  0x06,  0x07,  0x08,  0x09,  0x0a,  0x0b,  0x0c,  0x0d,  0x0e,  0x0f
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  false, true,  true,  false, true,  true,  // 0x00
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0x10
            true,  false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x20
            false, false, false, false, false, false, false, false, false, false, false, false, false, true,  false, false, // 0x30
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x40
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x50
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x60
            false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true,  // 0x70
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0x80
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0x90
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xa0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xb0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xc0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xd0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xe0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true   // 0xf0
        };

        // See http://www.ietf.org/rfc/rfc2047.txt 5. (3)
        private static readonly bool[] Rfc2047QuoteTable = new bool[]
        {
         // 0x00,  0x01,  0x02,  0x03,  0x04,  0x05,  0x06,  0x07,  0x08,  0x09,  0x0a,  0x0b,  0x0c,  0x0d,  0x0e,  0x0f
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0x00
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0x10
            false, false, true,  true,  true,  true,  true,  true,  true,  true,  false, false, true,  false, true,  false, // 0x20
            false, false, false, false, false, false, false, false, false, false, true,  true,  true,  true,  true,  true,  // 0x30
            true,  false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x40
            false, false, false, false, false, false, false, false, false, false, false, true,  true,  true,  true,  true,  // 0x50
            true,  false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, // 0x60
            false, false, false, false, false, false, false, false, false, false, false, true,  true,  true,  true,  true,  // 0x70
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0x80
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0x90
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xa0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xb0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xc0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xd0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  // 0xe0
            true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true,  true   // 0xf0
        };

        private DecodingState decodingState;
        private readonly Queue<byte> decodingPostponeQueue = new Queue<byte>(78);

        private readonly bool[] encodingQuoteTable;
        private EncodingState encodingState;
        private int encodingColumn;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Initializes a new instance of the <see cref="QuotedPrintableStream"/> class by calling
        /// <see cref="QuotedPrintableStream(Stream, int)"/>(<paramref name="stream"/>, 1024).</summary>
        public QuotedPrintableStream(Stream stream) : this(stream, Settings.InMemoryStreamBufferSize)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="QuotedPrintableStream"/> class by calling
        /// <see cref="QuotedPrintableStream(Stream, int, bool)"/>(<paramref name="stream"/>, 1024,
        /// <paramref name="rfc2047Mode"/>).</summary>
        public QuotedPrintableStream(Stream stream, bool rfc2047Mode) :
            this(stream, Settings.InMemoryStreamBufferSize, rfc2047Mode)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="QuotedPrintableStream"/> class by calling
        /// <see cref="QuotedPrintableStream(Stream, int, bool)"/>(<paramref name="stream"/>,
        /// <paramref name="bufferSize"/>, false).</summary>
        public QuotedPrintableStream(Stream stream, int bufferSize) : this(stream, bufferSize, false)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="QuotedPrintableStream"/> class.</summary>
        /// <param name="stream">The stream from/to which encoded data is read/written.</param>
        /// <param name="bufferSize">The size of the internal read and write buffers in bytes.</param>
        /// <param name="rfc2047Mode">The value <c>false</c> to use
        /// <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a> rules only; <c>true</c>, to also use
        /// <a href="http://www.ietf.org/rfc/rfc2047.txt">RFC 2047</a> rules for encoding and decoding.</param>
        /// <exception cref="ArgumentNullException"><paramref name="stream"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="bufferSize"/> is less than or equal to 0.
        /// </exception>
        /// <remarks>If <c>true</c> is passed for <paramref name="rfc2047Mode"/>, the <see cref="Read"/> method then
        /// additionally converts octets with the value 0x5F (code for the ASCII '_') to the value 0x20 (code for the
        /// ASCII ' '). Similarly, the <see cref="Write"/> method then converts the value 0x20 to the value 0x5F and
        /// quotes all characters except the ones mentioned in
        /// <a href="http://www.ietf.org/rfc/rfc2047.txt">RFC 2047</a> under 5. (3).</remarks>
        public QuotedPrintableStream(Stream stream, int bufferSize, bool rfc2047Mode) : base(stream, bufferSize)
        {
            this.encodingQuoteTable = rfc2047Mode ? Rfc2047QuoteTable : Rfc2045QuoteTable;
        }

        /// <summary>See <see cref="Stream.Read(byte[], int, int)"/>.</summary>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Putting the whole state machine into one method improves performance considerably.")]
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated with call to BufferHelper.AssertValidRange.")]
        public sealed override int Read(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            unchecked
            {
                ReadBuffer readBuffer = this.ReadBuffer;
                int index = offset;
                int pastEnd = offset + count;
                byte currentByte;

                // Only try to get more encoded data if we haven't yet copied anything into buffer
                while ((index < pastEnd) && ((readBuffer.Index < readBuffer.Count) ||
                    ((index == offset) && this.IsMoreEncodedDataAvailable(readBuffer))))
                {
                    switch (this.decodingState)
                    {
                        case DecodingState.Normal:
                            currentByte = readBuffer[readBuffer.Index++];

                            switch (currentByte)
                            {
                                case (byte)' ':
                                case (byte)'\t':
                                    this.decodingPostponeQueue.Enqueue(currentByte);
                                    this.decodingState = DecodingState.WhiteSpace;
                                    break;
                                case (byte)'=':
                                    this.decodingPostponeQueue.Enqueue(currentByte);
                                    this.decodingState = DecodingState.QuoteChar;
                                    break;
                                case (byte)'_':
                                    buffer[index++] = this.encodingQuoteTable == Rfc2047QuoteTable ? (byte)' ' : currentByte;
                                    break;
                                default:
                                    buffer[index++] = currentByte;
                                    break;
                            }

                            break;
                        case DecodingState.QuoteChar:
                            currentByte = readBuffer[readBuffer.Index++];
                            this.decodingPostponeQueue.Enqueue(currentByte);

                            switch (currentByte)
                            {
                                case (byte)' ':
                                case (byte)'\t':
                                    this.decodingState = DecodingState.WhiteSpace;
                                    break;
                                case (byte)'\r':
                                    this.decodingState = DecodingState.SpecialLineBreak;
                                    break;
                                default:
                                    this.decodingState = DecodeTable[currentByte] == 0xff ?
                                        DecodingState.AddPostponed : DecodingState.QuoteFirstNibble;
                                    break;
                            }

                            break;
                        case DecodingState.QuoteFirstNibble:
                            currentByte = readBuffer[readBuffer.Index++];
                            this.decodingPostponeQueue.Enqueue(currentByte);
                            byte nibbleCandidate = DecodeTable[currentByte];

                            if (nibbleCandidate == 0xff)
                            {
                                this.decodingState = DecodingState.AddPostponed;
                            }
                            else
                            {
                                this.decodingPostponeQueue.Dequeue(); // Remove the quote byte
                                byte firstNibble = DecodeTable[this.decodingPostponeQueue.Dequeue()];
                                this.decodingPostponeQueue.Clear();
                                buffer[index++] = (byte)((firstNibble << 4) + nibbleCandidate);
                                this.decodingState = DecodingState.Normal;
                            }

                            break;
                        case DecodingState.WhiteSpace:
                            currentByte = readBuffer[readBuffer.Index++];
                            this.decodingPostponeQueue.Enqueue(currentByte);

                            switch (currentByte)
                            {
                                case (byte)' ':
                                case (byte)'\t':
                                    break;
                                case (byte)'\r':
                                    this.decodingState = DecodingState.SpecialLineBreak;
                                    break;
                                default:
                                    // The white space wasn't trailing -> Add all to the output
                                    this.decodingState = DecodingState.AddPostponed;
                                    break;
                            }

                            break;
                        case DecodingState.SpecialLineBreak:
                            currentByte = readBuffer[readBuffer.Index++];

                            switch (currentByte)
                            {
                                case (byte)'\n':
                                    if (this.decodingPostponeQueue.Peek() == (byte)'=')
                                    {
                                        // Soft line break
                                        this.decodingPostponeQueue.Clear();
                                        this.decodingState = DecodingState.Normal;
                                    }
                                    else
                                    {
                                        // Real line break
                                        this.decodingPostponeQueue.Clear();
                                        this.decodingPostponeQueue.Enqueue((byte)'\r');
                                        this.decodingPostponeQueue.Enqueue((byte)'\n');
                                        this.decodingState = DecodingState.AddPostponed;
                                    }

                                    break;
                                default:
                                    // We only had a CR without a LF, which is illegal, see RFC5322 2.3. The best we can
                                    // do is to add all postponed bytes to the output.
                                    this.decodingPostponeQueue.Enqueue(currentByte);
                                    this.decodingState = DecodingState.AddPostponed;
                                    break;
                            }

                            break;
                        default: // This should be DecodingState.AddPostponed but doing so decreases code coverage
                            var postponedByte = this.decodingPostponeQueue.Dequeue();
                            buffer[index++] = (this.encodingQuoteTable == Rfc2047QuoteTable) && (postponedByte == (byte)'_') ?
                                (byte)' ' : postponedByte;

                            switch (this.decodingPostponeQueue.Count)
                            {
                                case 0:
                                    this.decodingState = DecodingState.Normal;
                                    break;
                                case 1:
                                    switch (this.decodingPostponeQueue.Peek())
                                    {
                                        case (byte)'=':
                                            this.decodingState = DecodingState.QuoteChar;
                                            break;
                                        case (byte)' ':
                                        case (byte)'\t':
                                            this.decodingState = DecodingState.WhiteSpace;
                                            break;
                                    }

                                    break;
                            }

                            break;
                    }
                }

                return index - offset;
            }
        }

        /// <summary>See <see cref="Stream.Write(byte[], int, int)"/>.</summary>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated with call to BufferHelper.AssertValidRange.")]
        public sealed override void Write(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            unchecked
            {
                WriteBuffer writeBuffer = this.WriteBuffer;
                int pastEnd = offset + count;
                byte currentByte;

                while ((offset < pastEnd) && ((writeBuffer.Count < writeBuffer.Capacity) || writeBuffer.Flush()))
                {
                    currentByte = buffer[offset];

                    switch (this.encodingState)
                    {
                        case EncodingState.Normal:
                            if (this.encodingQuoteTable[currentByte])
                            {
                                currentByte = (byte)'=';
                                this.encodingState = this.encodingColumn >= MaxEncodedLineLength - 5 ?
                                    EncodingState.SoftLineBreakQuote : EncodingState.QuoteCharacter;
                            }
                            else
                            {
                                if (this.encodingColumn == MaxEncodedLineLength - 3)
                                {
                                    currentByte = (byte)'=';
                                    this.encodingState = EncodingState.SoftLineBreakQuote;
                                }
                                else
                                {
                                    if (currentByte == (byte)' ')
                                    {
                                        // The different quote tables for RFC2045 & RFC2047 ensure that we only get
                                        // here in RFC2047 mode
                                        currentByte = (byte)'_';
                                    }

                                    ++offset;
                                }
                            }

                            break;
                        case EncodingState.QuoteCharacter:
                            currentByte = EncodeTable[currentByte >> 4];
                            this.encodingState = EncodingState.QuoteFirstNibble;
                            break;
                        case EncodingState.QuoteFirstNibble:
                            currentByte = EncodeTable[currentByte & 0xf];
                            this.encodingState = EncodingState.Normal;
                            ++offset;
                            break;
                        case EncodingState.SoftLineBreakQuote:
                            currentByte = (byte)'\r';
                            this.encodingState = EncodingState.SoftLineBreakCarriageReturn;
                            break;
                        default: // This should be EncodingState.SoftLineBreakCarriageReturn but doing so decreases code coverage
                            currentByte = (byte)'\n';
                            this.encodingState = EncodingState.Normal;
                            break;
                    }

                    writeBuffer[writeBuffer.Count++] = currentByte;
                    this.encodingColumn =
                        (currentByte == (byte)'\r') || (currentByte == (byte)'\n') ? 0 : this.encodingColumn + 1;
                }
            }
        }

        /// <summary>Gets the encoded length of an array segment.</summary>
        /// <param name="original">An array of bytes to encode.</param>
        /// <param name="offset">The zero-based byte offset in <paramref name="original"/> at which to begin encoding
        /// bytes.</param>
        /// <param name="count">The number of bytes to be encoded.</param>
        /// <param name="rfc2047Mode">The value <c>false</c> to use
        /// <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a> rules only; <c>true</c>, to also use
        /// <a href="http://www.ietf.org/rfc/rfc2047.txt">RFC 2047</a> rules for encoding and decoding.</param>
        /// <exception cref="ArgumentException">The sum of <paramref name="offset"/> and <paramref name="count"/> is
        /// greater than the length of <paramref name="original"/>.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="original"/> equals <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> and/or <paramref name="count"/> are
        /// negative.</exception>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated with call to BufferHelper.AssertValidRange.")]
        public static int GetEncodedLength(byte[] original, int offset, int count, bool rfc2047Mode)
        {
            BufferHelper.AssertValidRange(original, "original", offset, "offset", count, "count");

            unchecked
            {
                bool[] encodingQuoteTable = rfc2047Mode ? Rfc2047QuoteTable : Rfc2045QuoteTable;
                int encodedLength = 0;
                int currentEncodingColumn = 0;
                int pastEnd = offset + count;
                byte currentByte;

                for (; offset < pastEnd; ++offset)
                {
                    currentByte = original[offset];

                    if (encodingQuoteTable[currentByte])
                    {
                        if (currentEncodingColumn >= MaxEncodedLineLength - 5)
                        {
                            currentEncodingColumn = 3;
                            encodedLength += 6;
                        }
                        else
                        {
                            currentEncodingColumn += 3;
                            encodedLength += 3;
                        }
                    }
                    else
                    {
                        if (currentEncodingColumn == MaxEncodedLineLength - 3)
                        {
                            currentEncodingColumn = 0;
                            encodedLength += 3;
                        }

                        currentEncodingColumn =
                            (currentByte == (byte)'\r') || (currentByte == (byte)'\n') ? 0 : currentEncodingColumn + 1;
                        ++encodedLength;
                    }
                }

                return encodedLength;
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private bool IsMoreEncodedDataAvailable(ReadBuffer readBuffer)
        {
            // The test against this.decodingState should more logically appear at the second position in the while
            // loop in the Read method because there's no need test whether there's more encoded data available in
            // the buffer when the next byte will be drawn from the postpone queue anyway. However, doing so would
            // mean that 3 tests must be made for *each* byte in the encoded stream. Doing this test here means that
            // this property getter is called only when the buffer is empty and until the postpone queue has been
            // emptied. This leads to slightly more calls to this property getter, but in turn we eliminate one test
            // from the while loop.
            if ((this.decodingState == DecodingState.AddPostponed) || readBuffer.Fill())
            {
                return true;
            }

            switch (this.decodingState)
            {
                case DecodingState.QuoteChar:
                case DecodingState.QuoteFirstNibble:
                case DecodingState.SpecialLineBreak:
                    // We're at the end of the encoded stream. If there is incomplete quoting, we need to add the
                    // bytes to the output.
                    this.decodingState = DecodingState.AddPostponed;
                    return true;
                default:
                    // Whitespace at the end is still skipped.
                    return false;
            }
        }

        private enum DecodingState
        {
            /// <summary>The previous read byte was a normal byte, that is, not white space and not quoted.</summary>
            Normal,

            /// <summary>The previous read byte was a quote byte (0x3d), which was put into the postpone queue.
            /// </summary>
            QuoteChar,

            /// <summary>The previous read byte was the first nibble of a quoted byte, that is, it directly appeared
            /// after a quote byte (0x3d), both of which were put into the postpone queue.</summary>
            QuoteFirstNibble,

            /// <summary>The previous read byte was white space (0x09 or 0x20), which was put into the postpone queue.
            /// </summary>
            WhiteSpace,

            /// <summary>The previous read byte was a carriage return (0x0d) appearing after a quote byte (0x3d) and/or
            /// white space (0x09 or 0x20), all of which were put into the postpone queue.</summary>
            SpecialLineBreak,

            /// <summary>Quoting was found to be invalid or white space was not trailing -> dequeue postponed bytes.
            /// </summary>
            AddPostponed
        }

        private enum EncodingState
        {
            /// <summary>The previous byte was a normal byte, which was written to the underlying stream verbatim.
            /// </summary>
            Normal,

            /// <summary>The previous byte written to the underlying stream was the quote character '='.</summary>
            QuoteCharacter,

            /// <summary>The previous byte written to the underlying stream was the first nibble of a quote sequence.
            /// </summary>
            QuoteFirstNibble,

            /// <summary>The previous byte written to the underlying stream was the '=' character of a soft line break.
            /// </summary>
            SoftLineBreakQuote,

            /// <summary>The previous byte written to the underlying stream was the '\r' character of a soft line break.
            /// </summary>
            SoftLineBreakCarriageReturn
        }
    }
}
